/*
Copyright (c) 2008 Alexander K Syed
Based on banana.py
Copyright (c) 2001-2004 Twisted Matrix Laboratories.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package org.pixelami.twisted.spread.pb
{
	import flash.events.Event;
	
	import org.pixelami.twisted.errors.JellyableError;
	import org.pixelami.twisted.failure.IFailure;
	import org.pixelami.twisted.internet.defer.Deferred;
	import org.pixelami.twisted.spread.errors.DeadReferenceError;
	import org.pixelami.twisted.spread.flavors.Referencable;
	import org.pixelami.twisted.spread.jelly.Jelly;
	import org.pixelami.twisted.spread.jelly.Jellyable;
	import org.pixelami.twisted.utils.PyTuple;
	import org.pixelami.twisted.utils.__builtin__;

	
	public class Broker extends Banana
	{
		public static var MAX_BROKER_REFS:uint = 100;//?
		
		private var version:uint = 6;
		
		private var _currentLocalId:uint = 0;
		
		private var _currentRequestId:uint = 0;
		
		public var disconnected:int = 0;
		private var disconnects:Array;
		private var failures:Array;
		private var connects:Array;
		private var localObjects:Object;
		private var security:Object;
		private var pageProducers:Array;
		private var currentRequestID:int;
		private var currentLocalID:int;
		
		private var luids:Object;
		private var remotelyCachedObjects:Object;
		private var remotelyCachedLUIDs:Object;
		private var locallyCachedObjects:Object;
		private var waitingForAnswers:Object;
		
		private var serializingPerspective:Object;
		public var unserializingPerspective:Object;
		private var jellyMethod:Object;
		private var jellyArgs:Object;
		private var jellyKw:Object;
		private var maxBrokerRefsViolations:uint = 0;
		
		public var factory:Object;

		public function Broker()
		{
			this.disconnected = 0;
	        this.disconnects = [];
	        this.failures = [];
	        this.connects = [];
	        this.localObjects = {};
	        this.security = security;
	        this.pageProducers = [];
	        this.currentRequestID = 0;
	        this.currentLocalID = 0;
	        
	        /*
	        # Some terms:
	        #  PUID: process unique ID; return value of id() function.  type "int".
	        #  LUID: locally unique ID; an ID unique to an object mapped over this
	        #        connection. type "int"
	        #  GUID: (not used yet) globally unique ID; an ID for an object which
	        #        may be on a redirected or meta server.  Type as yet undecided.
	        # Dictionary mapping LUIDs to local objects.
	        # set above to allow root object to be assigned before connection is made
	        # this.localObjects = {}
	        # Dictionary mapping PUIDs to LUIDs.
	        */
	        this.luids = {}
	        /*
	        # Dictionary mapping LUIDs to local (remotely cached) objects. Remotely
	        # cached means that they're objects which originate here, and were
	        # copied remotely.
	        */
	        this.remotelyCachedObjects = {}
	        /*
	        # Dictionary mapping PUIDs to (cached) LUIDs
	        */
	        this.remotelyCachedLUIDs = {}
	        /*
	        # Dictionary mapping (remote) LUIDs to (locally cached) objects.
	        */
	        this.locallyCachedObjects = {}
	        this.waitingForAnswers = {}
		}
		
		public function  serialize(object:Object, perspective:Object=null, method:Object=null, args:Array=null, kw:Object=null):Object
		{
	        /*
	        Jelly an object according to the remote security rules for this broker.
	        */
			
			var fn:Function = function(x:Object):void{};
			
	        if(object is Deferred)
	        {
	            object.addCallbacks(this.serialize, fn,
	                                
	                [perspective,
	                method,
	                args,
	                kw
	                ])
	            return object
	        }
			
			/*
	        # XXX This call is NOT REENTRANT and testing for reentrancy is just
	        # crazy, so it likely won't be.  Don't ever write methods that call the
	        # broker's serialize() method recursively (e.g. sending a method call
	        # from within a getState (this causes concurrency problems anyway so
	        # you really, really shouldn't do it))
			*/
	        
	        //# this.jellier = _NetJellier(this)
	        this.serializingPerspective = perspective;
	        this.jellyMethod = method;
	        this.jellyArgs = args;
	        this.jellyKw = kw;
	        try
	        {
	            //trace("Jellying");
	            return Jelly.jelly(object, this.security, null, this);
	        }
	        finally
	        {
	            this.serializingPerspective = null;
	            this.jellyMethod = null;
	            this.jellyArgs = null;
	            this.jellyKw = null;
	        }
	        return null;
	 	}
		
		//TODO  sexp should be Array ?
	    public function unserialize(sexp:* , perspective:Object = null):Object
	    {
	        /*
	        Unjelly an sexp according to the local security rules for this broker.
	        */
	
	        this.unserializingPerspective = perspective
	        try
	        {
	            return Jelly.unjelly(sexp, this.security, null, this)
	        }
	        catch(e:Error)
	        {
	        	trace("unjelly error - "+e);
	        }
	        finally
	        {
	            this.unserializingPerspective = null;
	        }
	        return null
	    }
	    		
		

		public function connectionReady():void
		{
			dispatchEvent(new Event(Event.CONNECT));
		}
		
		override public function expressionReceived(obj:Object):void
		{
			//trace("incoming sexp - "+obj);
			// this should be an array with item[0] being the command
			var a:Array = obj as Array;
			
			var fn:Function = this["proto_"+obj[0]];
			
			var args:Array = a.splice(1);
			
			fn.apply(this,args);
		}
		
		protected function proto_version(v:uint):void
		{
			if(v == version) 
			{
				sendEncoded(new Array('version',6));
				connectionReady();
			}
		}
		
		private function proto_error(requestID:int, fail:*):void
		{
			trace("proto_error - "+requestID);
			trace(fail);
		}
		
		private function proto_answer(requestID:int, netResult:Object):void
		{
			var d:Deferred = this.waitingForAnswers[requestID];
			//trace("proto_answer - d id : "+d.ref_id);
			//this.waitingForAnswers[requestID].__del__();
        	delete this.waitingForAnswers[requestID];
        	//this.waitingForAnswers[requestID] = null;
        	////trace("proto_answer - d POST delete "+d.ref_id);
        	
        	var cbargs:Object = this.unserialize(netResult);
        	//trace("netresult = "+cbargs);
        	d.callback(cbargs)
		}
		
		public function  sendDecRef(objectID:Object):void
		{
	        /*
	        (internal) Send a DECREF directive.
	        */
	        this.sendCall("decref", objectID);
	 	}

		private function proto_decref(objectID:Object):void
		{
			/*
			(internal) Decrement the reference count of an object.
	
	        If the reference count is zero, it will free the reference to this
	        object.
	        */
	        
	        var refs:int = this.localObjects[objectID].decref();
	        //trace("proto_decref = "+refs);
	        if (refs == 0)
	        {
	            var puid:int = this.localObjects[objectID].object.processUniqueID();
	            delete this.luids[puid];
	            delete this.localObjects[objectID];
	        }
		}
		
		private function localObjectForID(luid:Object):Object
		{
	        /*
	        Get a local object for a locally unique ID.
	
	        I will return an object previously stored with
	        self.L{registerReference}, or C{None} if XXX:Unfinished thought:XXX
			*/
	        var lob:Object = this.localObjects[luid];
	        if (!lob)
	            return null;
	        return lob.object;
	 	}
		
		private function proto_message(requestID:int, objectID:Object, message:String, answerRequired:int, netArgs:Array, netKw:Object):void
		{
			this._recvMessage(this.localObjectForID, requestID, objectID, message, answerRequired, netArgs, netKw);
		}
		
		private function  proto_cachemessage(requestID:int, objectID:Object, message:String, answerRequired:int, netArgs:Array, netKw:Object):void
		{
	        this._recvMessage(this.cachedLocallyAs, requestID, objectID, message, answerRequired, netArgs, netKw);
		}

	    private function _recvMessage(findObjMethod:Function, requestID:int, objectID:Object, message:String, answerRequired:int, netArgs:Array, netKw:Object):void
	    {
	        /*
	        Received a message-send.
	
	        Look up message based on object, unserialize the arguments, and
	        invoke it with args, and send an 'answer' or 'error' response.
	        */
	        var errorStatus:Boolean = false;
	        
	        try
	        {
	            var object:Referencable = findObjMethod(objectID);
	            //trace("_recvMessage resolved object : "+object);
	            
	            if (!object)
	            {
	                throw new Error("Invalid Object ID");
	            }
	            var netResult:Object = object.remoteMessageReceived(this, message, netArgs, netKw);
	        }
	        catch (e:JellyableError)
	        {
	            //trace("_recvMessage JellyableError "+e);
	            errorStatus = true;
	            if (answerRequired)
	            {
	            	
	            	/*
	                # If the error is Jellyable or explicitly allowed via our
	                # security options, send it back and let the code on the
	                # other end deal with unjellying.  If it isn't Jellyable,
	                # wrap it in a CopyableFailure, which ensures it can be
	                # unjellied on the other end.  We have to do this because
	                # all errors must be sent back.
	                */
	                if((e is Jellyable) || this.security.isClassAllowed(e))
	                {
	                    this._sendError(e, requestID);
	                }
	                else
	                {
	                    this._sendError(new CopyableFailure(e), requestID)
	                }
	            }
	        }
	        catch(e:Error)
	        {
	            //trace("_recvMessage Error "+e);
	            errorStatus = true;
	            if (answerRequired)
	            {
	                //log.msg("Peer will receive following PB //traceback:", isError=True)
	                //trace("Peer will receive following PB //traceback:");
	                var f:CopyableFailure = new CopyableFailure();
	                this._sendError(f, requestID);
	            }
	            //log.err();
	        }
	        
			if(!errorStatus)
	        {
	            if (answerRequired)
	            {
	                if (netResult is Deferred) 
	                {
	                    //var args = (requestID,);
	                    var args:PyTuple = new PyTuple(requestID);
	                    netResult.addCallbacks(this._sendAnswer, this._sendFailureOrError,
	                                           args, {},
	                                           args, {});
	                    //# XXX Should this be done somewhere else?
	                }
	                else
	                {
	                    this._sendAnswer(netResult, requestID)
	                }
	            }
	        }
	    }

	    // success
	    private function _sendAnswer(netResult:*, requestID:int):void
	    {
	    	/*
	        (internal) Send an answer to a previously sent message.
	        */
	        this.sendCall("answer", requestID, netResult)
	    }
	    
	    // failure
	    private function _sendFailureOrError(fail:*, requestID:int):void
	    {
	        /*
	        Call L{_sendError} or L{_sendFailure}, depending on whether C{fail}
	        represents an L{Error} subclass or not.
	        */
	        if (fail.check(Error) == null)
	        {
	            this._sendFailure(fail, requestID);
	        }
	        else
	        {
	            this._sendError(fail, requestID);
	        }
	    }
	
	
	    private function  _sendFailure(fail:*, requestID:Object):void
	    {
	        /*
	        Log error and then send it.
	        */
	        //log.msg("Peer will receive following PB //traceback:")
	        //log.err(fail)
	        //trace("Peer will receive following PB //traceback:"+fail)
	        this._sendError(fail, requestID)
	    }
	
	    private function  _sendError(fail:*, requestID:Object):void
	    {
	        /*
	        (internal) Send an error for a previously sent message.
	        */
	        if (fail is IFailure)
	        {
	            //# If the failures value is jellyable or allowed through security,
	            //# send the value
	            if ((fail.value is Jellyable) ||
	                this.security.isClassAllowed(fail.value.__class__))
	            {    
	                fail = fail.value;
	            }
	            else if (! (fail is CopyableFailure))
	            {
	                // arrgh what is factory ?
	                //fail = __static__.failure2Copyable(fail, this.factory.unsafe//tracebacks);
	            }
	        }
	        if (fail is CopyableFailure)
	        {
	            // arghh what is factory ?
	            //fail.unsafe//tracebacks = this.factory.unsafe//tracebacks;
	        }
	        this.sendCall("error", requestID, this.serialize(fail))
	    }

	    
	    private function cacheLocally(cid:Object, instance:*):void
	    {
	    	/*
	        (internal)
	        Store a non-filled-out cached instance locally.
	        */
	        this.locallyCachedObjects[cid] = instance;
	    }

	    private function  cachedLocallyAs(cid:Object):*
	    {
	        var instance:Object = this.locallyCachedObjects[cid];
	        return instance;
	    }
		
		
		private function newLocalID():uint
		{
			return ++_currentLocalId;
		}
		
		private function newRequestID():uint
		{
			return ++_currentRequestId;
		}
		
		public function sendMessage(prefix:String, perspective:Object, objectID:Object, message:String, args:Array, kw:Object):Deferred
		{
			trace("pbBroker sendMessage");
        	
        	var answerRequired:uint = 1;
        	/*
        	var pbc = null;
        	var pbe = null;
        	
        	
        	
        	if(kw.hasOwnProperty('pbcallback'))
        	{
	            pbc = kw['pbcallback'];
	            delete kw['pbcallback'];
	        }
	            
	        if(kw.hasOwnProperty('pberrback'))
	        {
	            pbe = kw['pberrback'];
	            delete kw['pberrback'];
	        }
	            
	        if(kw.hasOwnProperty('pbanswer'))
	        {
	            if((!pbe) && (!pbc)) "You can't specify a no-answer requirement."
	            answerRequired = kw['pbanswer']
	            delete kw['pbanswer']
	        }
	        */
	        if(this.disconnected)
	            throw new DeadReferenceError("Calling Stale Broker")
	        
	        //trace("pbBroker sendMessage 2");
	        
	        var netArgs:Array;
	        var netKw:Object;
	        var _tupArgs:PyTuple = new PyTuple();
	        _tupArgs.append(args);
	        //trace("pbBroker sendMessage 3");
	        try
	        {
	            netArgs = this.serialize(_tupArgs, perspective, message) as Array;
	            netKw = this.serialize(kw, perspective, message);
	        }
	        catch(e:Error)
	        {
	            trace("bad serializing in Broker sendMessage - "+e);
	            //return defer.fail(new Failure())
	        }
	            
	        var requestID:uint = this.newRequestID();
	        //trace("pbBroker sendMessage 4");
	        var rval:Deferred;
	        if(answerRequired)
	        {
	            rval = new Deferred()
	            this.waitingForAnswers[requestID] = rval;
	            //trace("pbBroker sendMessage 5");
	            /*
	            if(pbc || pbe)
	            {
	                //log.msg('warning! using deprecated "pbcallback"');
	                //trace('warning! using deprecated "pbcallback"');
	                rval.addCallbacks(pbc, pbe);
	            }
	            */
	        }
	        else
	        {
	            rval = null;
	        }
	        this.sendCall(prefix+"message", requestID, objectID, message, answerRequired, netArgs, netKw);
	        return rval
		}
		
		public function sendCall(...exp):void
		{
	        /*
	        Utility method to send an expression to the other side of the connection.
	        */
	        trace('sendCall - '+exp);
	        this.sendEncoded(exp)
	 	}
	 	
	 	public function remoteForName(name:String):RemoteReference
	 	{
	        /*
	        Returns an object from the remote name mapping.
	
	        Note that this does not check the validity of the name, only
	        creates a translucent reference for it.
	        */
	        trace("Broker remoteForName = "+name);
	        return new RemoteReference(null, this, name, 0);
	  	}
	  	
	  	public function registerReference(object:Object):Object
	  	{
	        /*
	        Get an ID for a local object.
	
	        Store a persistent reference to a local object and map its id()
	        to a generated, session-unique ID and return that ID.
			*/
	        if( object == null) throw new Error("Assertion Error - object is null")// None
	        var puid:uint = object.processUniqueID();
	        var luid:Object = this.luids[puid];
	        //trace("registerReference - puid = "+puid);
	        //trace("registerReference - luid = "+luid);
	        if (!luid)
	        {
	            if (__builtin__.len(this.localObjects) > MAX_BROKER_REFS)
	            {
	                this.maxBrokerRefsViolations = this.maxBrokerRefsViolations + 1
	                if (this.maxBrokerRefsViolations > 3)
	                {
	                    this.transport.loseConnection();
	                    throw new Error("Maximum PB reference count exceeded. Goodbye.");
	                }
	                throw new Error("Maximum PB reference count exceeded.");
	            }
	            luid = this.newLocalID();
	            this.localObjects[luid] = new Local(object);
	            this.luids[puid] = luid;
	        }
	        else
	        {
	            this.localObjects[luid].incref();
	        }
	        return luid;
	    }
	    
	    public function setNameForLocal(name:String, object:Object):void
	    {
	        /*
	        Store a special (string) ID for this object.
	
	        This is how you specify a 'base' set of objects that the remote
	        protocol can connect to.
	        */
	        //assert object is not None
	        if( object == null) throw new Error("Assertion Error - object is null");
	        this.localObjects[name] = Local(object);
	    }
	}
}